﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SlimDX.DXGI;
using SlimDX.Direct3D10;
using System.Runtime.InteropServices;

namespace System.Windows.Media.Media3D10
{
    /// <summary>
    /// Modèle 3D pour une ellipse ou un cercle
    /// </summary>
    public sealed class EllipseModel3D10 : RenderModel3D10
    {
        #region Dependency Properties
        /// <summary>
        /// Identifie la propriété de dépendance <see cref="Eccentricity"/>.
        /// </summary>
        public static readonly DependencyProperty EccentricityProperty =
            DependencyProperty.Register("Eccentricity", typeof(float), typeof(EllipseModel3D10),
            new PropertyMetadata(0.0f, new PropertyChangedCallback(Eccentricity_PropertyChanged)));

        /// <summary>
        /// Identifie la propriété de dépendance <see cref="Radius"/>.
        /// </summary>
        public static readonly DependencyProperty RadiusProperty =
            DependencyProperty.Register("Radius", typeof(float), typeof(EllipseModel3D10),
            new PropertyMetadata(1.0f, new PropertyChangedCallback(Radius_PropertyChanged)));

        /// <summary>
        /// Identifie la propriété de dépendance <see cref="Divisions"/>.
        /// </summary>
        public static readonly DependencyProperty DivisionsProperty =
            DependencyProperty.Register("Divisions", typeof(int), typeof(EllipseModel3D10),
            new PropertyMetadata(128, new PropertyChangedCallback(Divisions_PropertyChanged)));

        /// <summary>
        /// Identifie la propriété de dépendance <see cref="Brush"/>.
        /// </summary>
        public static readonly DependencyProperty BrushProperty =
            DependencyProperty.Register("Brush", typeof(Brush), typeof(EllipseModel3D10),
            new PropertyMetadata(new PropertyChangedCallback(Brush_PropertyChanged)));
        #endregion

        #region Dependency Properties Callbacks
        private static void Eccentricity_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(sender, null))
                throw new ArgumentNullException("sender");
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            float value = (float)e.NewValue;
            if (value < 0 || value >= 1) throw new ArgumentOutOfRangeException("e");
            (sender as EllipseModel3D10).OnEccentricityChanged(e);
        }

        private static void Radius_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(sender, null))
                throw new ArgumentNullException("sender");
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            (sender as EllipseModel3D10).OnRadiusChanged(e);
        }

        private static void Divisions_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(sender, null))
                throw new ArgumentNullException("sender");
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            (sender as EllipseModel3D10).OnDivisionsyChanged(e);
        }

        private static void Brush_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(sender, null))
                throw new ArgumentNullException("sender");
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            (sender as EllipseModel3D10).OnBrushChanged(e);
        }
        #endregion

        #region Data
        private BufferableCollection<SlimDX.Color4> _colors;
        private LineGeometry3D10 _geometry;
        #endregion

        #region Public Properties
        /// <summary>
        /// Eccentricité de l'ellipse
        /// </summary>
        public float Eccentricity
        {
            get { return (float)GetValue(EccentricityProperty); }
            set { SetValue(EccentricityProperty, value); }
        }

        /// <summary>
        /// Rayont de l'ellipse
        /// </summary>
        public float Radius
        {
            get { return (float)GetValue(RadiusProperty); }
            set { SetValue(RadiusProperty, value); }
        }

        /// <summary>
        /// Nombre de division à utiliser pour générer le Mesh
        /// </summary>
        public int Divisions
        {
            get { return (int)GetValue(DivisionsProperty); }
            set { SetValue(DivisionsProperty, value); }
        }

        /// <summary>
        /// Couleur de l'ellipse
        /// </summary>
        public Brush Brush
        {
            get { return (Brush)GetValue(BrushProperty); }
            set { SetValue(BrushProperty, value); }
        }
        #endregion

        #region Private Members
        private void OnEccentricityChanged(DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            ComputeGeometry();
        }

        private void OnRadiusChanged(DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            ComputeGeometry();
        }

        private void OnDivisionsyChanged(DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            ComputeGeometry();
        }

        private void OnBrushChanged(DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            ComputeGeometry();
        }

        private void ComputeGeometry()
        {
            _geometry = new LineGeometry3D10();
            if (Divisions == 0 || Radius == 0 || Brush == null) return;
            
            float e = this.Eccentricity;
            float a = this.Radius;
            float b = a * (float)Math.Sqrt(1 - e * e);
            int div = this.Divisions;
            float dt = 2 * (float)Math.PI / div;
            
            for (int t = 0; t <= div; t++)
            {
                float x = a * (float)Math.Cos(dt * t);
                float y = b * (float)Math.Sin(dt * t);

                _geometry.Positions.Add(new SlimDX.Vector3(x, y, 0));
            }
            _geometry.Freeze();

            SolidColorBrush solidColorBrush = Brush as SolidColorBrush;
            LinearGradientBrush linearGradientBrush = Brush as LinearGradientBrush;
            RadialGradientBrush radialGradientBrush = Brush as RadialGradientBrush;
            if (solidColorBrush != null) ComputeColors(solidColorBrush);
            else if (linearGradientBrush != null) ComputeColors(linearGradientBrush.GradientStops);
            else if (radialGradientBrush != null) ComputeColors(radialGradientBrush.GradientStops);
            else throw new NotImplementedException();

            InvalidateInputLayout();
            InvalidateVertexBufferBinding();
        }

        private void ComputeColors(SolidColorBrush brush)
        {
            _colors = new BufferableCollection<SlimDX.Color4>(BindFlags.VertexBuffer);
            for (int i = 0; i < _geometry.Positions.Count; i++)
            {
                _colors.Add(new SlimDX.Color4(brush.Color.A, brush.Color.R, brush.Color.G, brush.Color.B));
            }
            _colors.Freeze();
        }

        private void ComputeColors(GradientStopCollection gradientStops)
        {
            _colors = new BufferableCollection<SlimDX.Color4>(BindFlags.VertexBuffer);
            int count = _geometry.Positions.Count;
            IEnumerable<GradientStop> orderedGs = gradientStops.OrderByDescending(g => g.Offset).ToList();
            for (int i = 0; i < count; i++)
            {
                double offset = (double)i / (double)count;
                GradientStop gsMin = orderedGs.First(g => g.Offset <= offset);
                GradientStop gsMax = orderedGs.Last(g => g.Offset >= offset);
                double ratioMin = gsMin.Offset == gsMax.Offset ? 1 : (gsMax.Offset - offset) / (gsMax.Offset - gsMin.Offset);
                double ratioMax = gsMin.Offset == gsMax.Offset ? 0 : 1 - ratioMin;
                Color colorMin = gsMin.Color;
                Color colorMax = gsMax.Color;
                int alpha = (int)(colorMin.A * ratioMin + colorMax.A * ratioMax);
                int red = (int)(colorMin.R * ratioMin + colorMax.R * ratioMax);
                int green = (int)(colorMin.G * ratioMin + colorMax.G * ratioMax);
                int blue = (int)(colorMin.B * ratioMin + colorMax.B * ratioMax);
                _colors.Add(new SlimDX.Color4(alpha, red, green, blue));
            }
            _colors.Freeze();
        }
        #endregion

        #region Model3D10 Overrides
        /// <summary>
        /// En cas de substitution dans une classe dérivée, participe au dessin des objets dans le pipeline DirectX10
        /// </summary>
        /// <param name="context">Contexte de dessin.</param>
        /// <exception cref="ArgumentNullException">Soulevée si le contexte est null.</exception>
        protected override void OnDraw(DrawingContext context)
        {
            if (object.ReferenceEquals(context, null))
                throw new ArgumentNullException("context");
            base.OnDraw(context);

            context.Draw(this, PrimitiveTopology.LineStrip, this._geometry.Positions.Count);
        }
        #endregion

        #region RenderModel3D10
        /// <summary>
        /// Crée le <see cref="InputLayout"/> associé à ce <see cref="RenderModel3D10"/>
        /// </summary>
        /// <returns></returns>
        protected override InputLayout CreateInputLayoutCore()
        {
            if (_geometry == null || !_geometry.IsFrozen)
                return null;
            InputElement[] elements = _geometry.GetInputElements();
            Array.Resize<InputElement>(ref elements, elements.Length + 1);
            elements[elements.Length - 1] = new InputElement("COLOR", 0, Format.R32G32B32A32_Float, 0, 1);
            return CurrentEffectTechnique == null ? null : CurrentEffectTechnique.CreateInputLayout(elements);
        }

        /// <summary>
        /// Crée le <see cref="VertexBufferBinding"/> associé à ce <see cref="RenderModel3D10"/>
        /// </summary>
        /// <returns></returns>
        protected override VertexBufferBinding[] CreateVertexBufferBindingCore()
        {
            if (_geometry == null || !_geometry.IsFrozen)
                return null;
            VertexBufferBinding[] binding = _geometry.GetVertexBufferBinding();
            Array.Resize<VertexBufferBinding>(ref binding, binding.Length + 1);
            binding[binding.Length - 1] = new SlimDX.Direct3D10.VertexBufferBinding(_colors.Buffer, Marshal.SizeOf(typeof(SlimDX.Color4)), 0);
            return binding;
        }
        #endregion

        #region Freezable Implementation
        /// <summary>
        /// Une fois implémentée dans une classe dérivée, initialise une nouvelle instance de la classe dérivée <see cref="Freezable"/>. 
        /// </summary>
        /// <returns>Nouvelle instance.</returns>
        protected override Freezable CreateInstanceCore()
        {
            return new EllipseModel3D10();
        }
        #endregion
    }
}
